/*
 * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.analysis.decompiler.stub

import com.intellij.openapi.diagnostic.Logger
import com.intellij.psi.PsiElement
import com.intellij.psi.stubs.StubElement
import com.intellij.util.io.StringRef
import org.jetbrains.kotlin.analysis.decompiler.stub.flags.*
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.builtins.isNumberedFunctionClassFqName
import org.jetbrains.kotlin.descriptors.SourceElement
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.*
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.ClassIdBasedLocality
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtSuperTypeEntry
import org.jetbrains.kotlin.psi.KtSuperTypeList
import org.jetbrains.kotlin.psi.stubs.elements.KotlinValueClassRepresentation
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
import org.jetbrains.kotlin.psi.stubs.impl.*
import org.jetbrains.kotlin.serialization.deserialization.ProtoContainer
import org.jetbrains.kotlin.serialization.deserialization.getClassId
import org.jetbrains.kotlin.serialization.deserialization.getName
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty

fun createClassStub(
    parent: StubElement<out PsiElement>,
    classProto: ProtoBuf.Class,
    nameResolver: NameResolver,
    classId: ClassId,
    source: SourceElement?,
    context: ClsStubBuilderContext,
) {
    ClassClsStubBuilder(parent, classProto, nameResolver, classId, source, context).build()
}

private class ClassClsStubBuilder(
    private val parentStub: StubElement<out PsiElement>,
    private val classProto: ProtoBuf.Class,
    nameResolver: NameResolver,
    private val classId: ClassId,
    source: SourceElement?,
    outerContext: ClsStubBuilderContext,
) {
    private val thisAsProtoContainer = ProtoContainer.Class(
        classProto, nameResolver, TypeTable(classProto.typeTable), source, outerContext.protoContainer
    )
    private val classKind = thisAsProtoContainer.kind

    private val c = outerContext.child(
        classProto.typeParameterList, classId.shortClassName, nameResolver, thisAsProtoContainer.typeTable, thisAsProtoContainer
    )
    private val typeStubBuilder = TypeClsStubBuilder(c)
    private val supertypeIds = run {
        val supertypeIds = classProto.supertypes(c.typeTable).map { c.nameResolver.getClassId(it.className) }
        //empty supertype list if single supertype is Any
        if (supertypeIds.singleOrNull()?.let { StandardNames.FqNames.any == it.asSingleFqName().toUnsafe() } == true) {
            listOf()
        } else {
            supertypeIds
        }
    }

    private val companionObjectName = if (classProto.hasCompanionObjectName())
        c.nameResolver.getName(classProto.companionObjectName)
    else
        null

    private val classOrObjectStub = createClassOrObjectStubAndModifierListStub()

    fun build() {
        val typeConstraintListData = typeStubBuilder.createTypeParameterListStub(classOrObjectStub, classProto.typeParameterList)
        createConstructorStub()
        createDelegationSpecifierList()
        typeStubBuilder.createTypeConstraintListStub(classOrObjectStub, typeConstraintListData)
        createClassBodyAndMemberStubs()
    }

    private fun createClassOrObjectStubAndModifierListStub(): StubElement<out PsiElement> {
        val classOrObjectStub = doCreateClassOrObjectStub()
        val modifierList = createModifierListForClass(classOrObjectStub)
        typeStubBuilder.createContextReceiverStubs(modifierList, classProto.contextReceiverTypes(c.typeTable))
        if (Flags.HAS_ANNOTATIONS.get(classProto.flags)) {
            createAnnotationStubs(c.components.annotationLoader.loadClassAnnotations(thisAsProtoContainer), modifierList)
        }
        return classOrObjectStub
    }

    private fun createModifierListForClass(parent: StubElement<out PsiElement>): KotlinModifierListStubImpl {
        val relevantFlags = arrayListOf(
            VISIBILITY,
            EXTERNAL_CLASS,
            EXPECT_CLASS,
            INNER,
            DATA,
            VALUE_CLASS,
            FUN_INTERFACE,
        )

        when {
            isInterface() -> relevantFlags.add(INTERFACE_MODALITY)
            isObject() -> {} // objects are final always
            else -> relevantFlags.add(MODALITY)
        }

        val additionalModifiers = when (classKind) {
            ProtoBuf.Class.Kind.ENUM_CLASS -> listOf(KtTokens.ENUM_KEYWORD)
            ProtoBuf.Class.Kind.COMPANION_OBJECT -> listOf(KtTokens.COMPANION_KEYWORD)
            ProtoBuf.Class.Kind.ANNOTATION_CLASS -> listOf(KtTokens.ANNOTATION_KEYWORD)
            else -> emptyList()
        }

        return createModifierListStubForDeclaration(
            parent,
            classProto.flags,
            relevantFlags,
            additionalModifiers,
            returnValueStatus = null,
        )
    }

    private fun doCreateClassOrObjectStub(): StubElement<out PsiElement> {
        val fqName = classId.asSingleFqName()
        val shortName = fqName.shortName().ref()
        val superTypeRefs = supertypeIds.filterNot {
            //TODO: filtering function types should go away
            isNumberedFunctionClassFqName(it.asSingleFqName().toUnsafe())
        }.map { it.shortClassName.ref() }.ifNotEmpty { toTypedArray() } ?: StringRef.EMPTY_ARRAY

        @OptIn(ClassIdBasedLocality::class)
        val classId = classId.takeUnless { it.isLocal }
        val isTopLevel = classId?.isNestedClass == false
        return when (classKind) {
            ProtoBuf.Class.Kind.OBJECT, ProtoBuf.Class.Kind.COMPANION_OBJECT -> {
                KotlinObjectStubImpl(
                    parentStub, shortName, fqName,
                    classId = classId,
                    superTypeRefs,
                    isTopLevel = isTopLevel,
                    isLocal = false,
                    isObjectLiteral = false,
                )
            }

            ProtoBuf.Class.Kind.ENUM_ENTRY -> error("Enum entries have to be created as members via '${::createEnumEntryStubs.name}'")

            else -> {
                KotlinClassStubImpl(
                    parent = parentStub,
                    qualifiedName = fqName.ref(),
                    classId = classId,
                    name = shortName,
                    superNameRefs = superTypeRefs,
                    isInterface = classKind == ProtoBuf.Class.Kind.INTERFACE,
                    isClsStubCompiledToJvmDefaultImplementation = JvmProtoBufUtil.isNewPlaceForBodyGeneration(classProto),
                    isLocal = false,
                    isTopLevel = isTopLevel,
                    valueClassRepresentation = valueClassRepresentation(),
                )
            }
        }
    }

    private fun valueClassRepresentation(): KotlinValueClassRepresentation? = when {
        classProto.hasInlineClassUnderlyingPropertyName() -> KotlinValueClassRepresentation.INLINE_CLASS
        else -> null
    }

    private fun createConstructorStub() {
        if (!isClass()) return

        val primaryConstructorProto = classProto.constructorList.find { !Flags.IS_SECONDARY.get(it.flags) } ?: return

        createConstructorStub(classOrObjectStub, primaryConstructorProto, c, thisAsProtoContainer)
    }

    private fun createDelegationSpecifierList() {
        // if single supertype is any then no delegation specifier list is needed
        if (supertypeIds.isEmpty()) return

        val delegationSpecifierListStub = KotlinPlaceHolderStubImpl<KtSuperTypeList>(classOrObjectStub, KtStubElementTypes.SUPER_TYPE_LIST)

        classProto.supertypes(c.typeTable).forEach { type ->
            val superClassStub = KotlinPlaceHolderStubImpl<KtSuperTypeEntry>(
                delegationSpecifierListStub, KtStubElementTypes.SUPER_TYPE_ENTRY
            )
            typeStubBuilder.createTypeReferenceStub(superClassStub, type)
        }
    }

    private fun createClassBodyAndMemberStubs() {
        val classBody = KotlinPlaceHolderStubImpl<KtClassBody>(classOrObjectStub, KtStubElementTypes.CLASS_BODY)
        createEnumEntryStubs(classBody)
        createCompanionObjectStub(classBody)
        createCallableMemberStubs(classBody)
        createInnerAndNestedClasses(classBody)
        createTypeAliasesStubs(classBody)
    }

    private fun createCompanionObjectStub(classBody: KotlinPlaceHolderStubImpl<KtClassBody>) {
        if (companionObjectName == null) {
            return
        }

        val companionObjectId = classId.createNestedClassId(companionObjectName)
        createNestedClassStub(classBody, companionObjectId)
    }

    private fun createEnumEntryStubs(classBody: KotlinPlaceHolderStubImpl<KtClassBody>) {
        if (classKind != ProtoBuf.Class.Kind.ENUM_CLASS) return

        classProto.enumEntryList.forEach { entry ->
            val name = c.nameResolver.getName(entry.name)
            val annotations = c.components.annotationLoader.loadEnumEntryAnnotations(thisAsProtoContainer, entry)
            val enumEntryStub = KotlinEnumEntryStubImpl(
                classBody,
                qualifiedName = c.containerFqName.child(name).ref(),
                name = name.ref(),
                isLocal = false,
            )

            if (annotations.isNotEmpty()) {
                createAnnotationStubs(annotations, createEmptyModifierListStub(enumEntryStub))
            }
        }
    }

    private fun createCallableMemberStubs(classBody: KotlinPlaceHolderStubImpl<KtClassBody>) {
        for (secondaryConstructorProto in classProto.constructorList) {
            if (Flags.IS_SECONDARY.get(secondaryConstructorProto.flags)) {
                createConstructorStub(classBody, secondaryConstructorProto, c, thisAsProtoContainer)
            }
        }

        createDeclarationsStubs(classBody, c, thisAsProtoContainer, classProto.functionList, classProto.propertyList)
    }

    private fun isClass(): Boolean = when (classKind) {
        ProtoBuf.Class.Kind.CLASS, ProtoBuf.Class.Kind.ENUM_CLASS, ProtoBuf.Class.Kind.ANNOTATION_CLASS -> true
        else -> false
    }

    private fun isObject(): Boolean = when (classKind) {
        ProtoBuf.Class.Kind.OBJECT, ProtoBuf.Class.Kind.COMPANION_OBJECT -> true
        else -> false
    }

    private fun isInterface(): Boolean = classKind == ProtoBuf.Class.Kind.INTERFACE

    private fun createInnerAndNestedClasses(classBody: KotlinPlaceHolderStubImpl<KtClassBody>) {
        classProto.nestedClassNameList.forEach { id ->
            val nestedClassName = c.nameResolver.getName(id)
            if (nestedClassName != companionObjectName) {
                val nestedClassId = classId.createNestedClassId(nestedClassName)
                createNestedClassStub(classBody, nestedClassId)
            }
        }
    }

    private fun createTypeAliasesStubs(classBody: KotlinPlaceHolderStubImpl<KtClassBody>) {
        createTypeAliasesStubs(classBody, c, thisAsProtoContainer, classProto.typeAliasList)
    }

    private fun createNestedClassStub(classBody: StubElement<out PsiElement>, nestedClassId: ClassId) {
        val (nameResolver, classProto, _, sourceElement) =
            c.components.classDataFinder.findClassData(nestedClassId)
                ?: c.components.virtualFileForDebug.let { rootFile ->
                    if (LOG.isDebugEnabled) {
                        val outerClassId = nestedClassId.outerClassId
                        val sortedChildren = rootFile.parent.children.sortedBy { it.name }
                        val msgPrefix = "Could not find data for nested class $nestedClassId of class $outerClassId\n"
                        val explanation = when {
                            outerClassId != null && sortedChildren.none { it.name.startsWith("${outerClassId.relativeClassName}\$a") } ->
                                // KT-29427: case with obfuscation
                                "Reason: obfuscation suspected (single-letter name)\n"
                            else ->
                                // General case
                                ""
                        }
                        val msg = msgPrefix + explanation +
                                "Root file: ${rootFile.canonicalPath}\n" +
                                "Dir: ${rootFile.parent.canonicalPath}\n" +
                                "Children:\n" +
                                sortedChildren.joinToString(separator = "\n") {
                                    "${it.name} (valid: ${it.isValid})"
                                }
                        LOG.debug(msg)
                    }
                    return
                }
        if (nestedClassId == nameResolver.getClassId(classProto.fqName)) {
            createClassStub(classBody, classProto, nameResolver, nestedClassId, sourceElement, c)
        }
    }

    companion object {
        private val LOG = Logger.getInstance(ClassClsStubBuilder::class.java)
    }
}
